package speed2010.andreza.vspeed.view;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.MouseEvent;

import javax.swing.BorderFactory;
import javax.swing.SwingConstants;

import prefuse.Display;
import prefuse.Visualization;
import prefuse.action.ActionList;
import prefuse.action.RepaintAction;
import prefuse.action.assignment.ColorAction;
import prefuse.action.filter.GraphDistanceFilter;
import prefuse.action.layout.graph.ForceDirectedLayout;
import prefuse.activity.Activity;
import prefuse.controls.ControlAdapter;
import prefuse.controls.DragControl;
import prefuse.controls.FocusControl;
import prefuse.controls.NeighborHighlightControl;
import prefuse.controls.PanControl;
import prefuse.controls.WheelZoomControl;
import prefuse.controls.ZoomControl;
import prefuse.controls.ZoomToFitControl;
import prefuse.data.Graph;
import prefuse.data.Schema;
import prefuse.data.Tuple;
import prefuse.data.event.TupleSetListener;
import prefuse.data.tuple.TupleSet;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.EdgeRenderer;
import prefuse.render.LabelRenderer;
import prefuse.util.ColorLib;
import prefuse.util.FontLib;
import prefuse.util.PrefuseLib;
import prefuse.util.force.ForceSimulator;
import prefuse.util.ui.JFastLabel;
import prefuse.util.ui.JSearchPanel;
import prefuse.visual.VisualGraph;
import prefuse.visual.VisualItem;
import prefuse.visual.expression.InGroupPredicate;

/**
 * This class creates a display for a graph.
 * <p/>
 * Project OWL2Prefuse <br/>
 * SimpleGraphjava created 3 januari 2007, 11:17
 * <p/>
 * Copyright &copy 2006 Jethro Borsje
 * 
 * @author <a href="mailto:info@jborsje.nl">Jethro Borsje</a>
 * @version $$Revision:$$, $$Date:$$
 */
public class GraphDisplay extends Display {
	private static final long serialVersionUID = -6403672168741846390L;

	/**
	 * Create data description of labels, setting colors, and fonts ahead of time
	 */
	private static final Schema DECORATOR_SCHEMA = PrefuseLib.getVisualItemSchema();
	static {
		DECORATOR_SCHEMA.setDefault(VisualItem.INTERACTIVE, false);
		DECORATOR_SCHEMA.setDefault(VisualItem.TEXTCOLOR, ColorLib.gray(0));
		DECORATOR_SCHEMA.setDefault(VisualItem.FONT, FontLib.getFont("Tahoma", 10));
	}

	/**
	 * The searchpanel, used for the keyword search in the graph.
	 */
	private JSearchPanel m_search;

	/**
	 * The label which displays the URI of the node under the mouse.
	 */
	private JFastLabel m_URILabel;

	/**
	 * The GraphDistanceFilter, which makes sure that only the nodes with a certain number of hops away from the
	 * currently selected node, are displayed.
	 */
	private GraphDistanceFilter m_filter;

	/**
	 * The focus control of the graph.
	 */
	private FocusControl m_focusControl;

	private QueryFocusControl doubleClickControl;

	/**
	 * The force directed layout.
	 */
	private ForceDirectedLayout m_fdl;

	/**
	 * The force simulator of the force directed layout.
	 */
	private ForceSimulator m_fsim;

	private final Object targetObject;

	/**
	 * Creates a new instance of GraphDisplay
	 * 
	 * @param p_graph
	 *            The Prefuse Graph to be displayed.
	 * @param p_distancefilter
	 *            A boolean, indicating whether or not a GraphDistance filter should be used with this display.
	 */
	public GraphDisplay(final Graph p_graph, final boolean p_distancefilter, final Object targetObject) {
		// Create a new Display with an empty visualization.
		super(new Visualization());

		this.targetObject = targetObject;

		initVisualization(p_graph, p_distancefilter);
		initDisplay();

		// Create the search panel.
		createSearchPanel();

		// Create the title label.
		createTitleLabel();

		this.m_vis.run("draw");
	}

	/**
	 * Add the graph to the visualization as the data group "graph" nodes and edges are accessible as "graph.nodes" and
	 * "graph.edges". A renderer is created to render the edges and the nodes in the graph.
	 * 
	 * @param p_graph
	 *            The Prefuse Graph to be displayed.
	 * @param p_distancefilter
	 *            A boolean, indicating whether or not a GraphDistance filter should be used with this display.
	 */
	private void initVisualization(final Graph p_graph, final boolean p_distancefilter) {
		// Add the graph to the visualization.
		final VisualGraph vg = this.m_vis.addGraph(PrefuseConstants.GRAPH, p_graph);

		// Set up a label renderer for the labels on the nodes and add it to the
		// visualization.
		final LabelRenderer nodeRenderer = new LabelRenderer("name");
		nodeRenderer.setRoundedCorner(8, 8);

		final DefaultRendererFactory drf = new DefaultRendererFactory();
		drf.add(new InGroupPredicate(PrefuseConstants.GRAPH_NODES), nodeRenderer);
		drf.add(new InGroupPredicate(PrefuseConstants.EDGE_DECORATORS), new LabelRenderer("label"));
		drf.add(new InGroupPredicate(PrefuseConstants.GRAPH_EDGES), new EdgeRenderer(prefuse.Constants.EDGE_TYPE_LINE,
				prefuse.Constants.EDGE_ARROW_FORWARD));
		this.m_vis.setRendererFactory(drf);

		// Add the decorator for the labels.
		this.m_vis.addDecorators(PrefuseConstants.EDGE_DECORATORS, PrefuseConstants.GRAPH_EDGES, DECORATOR_SCHEMA);

		// Set the interactive value of the edges to false.
		this.m_vis.setValue(PrefuseConstants.GRAPH_EDGES, null, VisualItem.INTERACTIVE, Boolean.FALSE);

		// Get the first node and give it focus, this triggers the distance filter
		// to at least show all nodes with a maximum of 4 hops away from this one.
		if (p_distancefilter) {
			final VisualItem f = (VisualItem) vg.getNode(0);
			this.m_vis.getGroup(Visualization.FOCUS_ITEMS).setTuple(f);

			// The position of the first node is not fixed.
			f.setFixed(false);
		}

		// Create a focus listener which fixex the position of the selected nodes.
		final TupleSet focusGroup = this.m_vis.getGroup(Visualization.FOCUS_ITEMS);
		focusGroup.addTupleSetListener(getFocusedItemsListner());

		// Finally, we register our ActionList with the Visualization.
		// We can later execute our Actions by invoking a method on our Visualization,
		// using the name we have chosen below.
		this.m_vis.putAction("draw", getDrawActions(p_distancefilter));
		this.m_vis.putAction("layout", getLayoutActions());
		this.m_vis.runAfter("draw", "layout");
	}

	/**
	 * Initialize this display. This method adds several control listeners, sets the size and sets the foreground and
	 * background colors.
	 */
	private void initDisplay() {
		// Set the display size.
		setSize(500, 500);

		// Set the foreground color.
		setForeground(PrefuseConstants.FOREGROUND);

		// Set the background color.
		setBackground(PrefuseConstants.BACKGROUND);

		// Drag items around.
		addControlListener(new DragControl());

		// Pan with background left-drag.
		addControlListener(new PanControl());

		// Zoom with vertical right-drag.
		addControlListener(new ZoomControl());

		// Zoom using the scroll wheel.
		addControlListener(new WheelZoomControl());

		// Double click for zooming to fit the graph.
		addControlListener(new ZoomToFitControl());

		// Highlight the neighbours.
		addControlListener(new NeighborHighlightControl());

		// Conrol which nodes are in focus.
		this.m_focusControl = new FocusControl(1);
		addControlListener(this.m_focusControl);

		this.doubleClickControl = new QueryFocusControl(2, this.targetObject);
		addControlListener(this.doubleClickControl);
	}

	/**
	 * Get the focus control of the graph, so it can be adjusted or even removed to implement a custom made focus
	 * control.
	 * 
	 * @return The focus control of the graph.
	 */
	public FocusControl getFocusControl() {
		return this.m_focusControl;
	}

	/**
	 * This metodh creates a TupleSetListener which listens for changes in a tuple set.
	 * 
	 * @return A TupleSetListener.
	 */
	private TupleSetListener getFocusedItemsListner() {
		final TupleSetListener listner = new TupleSetListener() {
			@Override
			public void tupleSetChanged(final TupleSet ts, final Tuple[] add, final Tuple[] rem) {
				// Set the fixed attribute for the nodes that are no longer in focus
				// to false.
				for (int i = 0; i < rem.length; ++i) {
					((VisualItem) rem[i]).setFixed(false);
				}

				// Set the fixed attribute for the nodes that are added to the focus
				// to true.
				for (int i = 0; i < add.length; ++i) {
					((VisualItem) add[i]).setFixed(false);
					((VisualItem) add[i]).setFixed(true);
				}

				// If there are no nodes in focus, get the first one that is to
				// be removed, add it to the tuple set en set it' fixed attribute
				// to false. Thereby intializing the tupleset with one node.
				if (ts.getTupleCount() == 0) {
					ts.addTuple(rem[0]);
					((VisualItem) rem[0]).setFixed(false);
				}

				// Run the draw action.
				GraphDisplay.this.m_vis.run("draw");
			}
		};

		return listner;
	}

	/**
	 * Returns the actionlist for the colors of our graph.
	 * 
	 * @param p_distancefilter
	 *            A boolean, indicating whether or not a GraphDistance filter should be used with this display.
	 * @return The actionlist containing the actions for the colors.
	 */
	private ActionList getDrawActions(final boolean p_distancefilter) {
		// Create an action list containing all color assignments.
		final ActionList draw = new ActionList();

		// Create the graph distance filter, if wanted.
		if (p_distancefilter) {
			this.m_filter = new GraphDistanceFilter(PrefuseConstants.GRAPH, 4);
			draw.add(this.m_filter);
		}

		// Use black for the text on the nodes.
		draw.add(new ColorAction(PrefuseConstants.GRAPH_NODES, VisualItem.TEXTCOLOR, ColorLib.rgb(0, 0, 0)));
		draw.add(new ColorAction(PrefuseConstants.GRAPH_NODES, VisualItem.STROKECOLOR, ColorLib.gray(0)));

		// Use light grey for edges.
		draw.add(new ColorAction(PrefuseConstants.GRAPH_EDGES, VisualItem.STROKECOLOR, ColorLib.gray(200)));
		draw.add(new ColorAction(PrefuseConstants.GRAPH_EDGES, VisualItem.FILLCOLOR, ColorLib.gray(200)));

		return draw;
	}

	/**
	 * This method sets the parameter of the force directed layout that determines the distance between the nodes (e.g.
	 * the length of the edges).
	 * 
	 * @param p_value
	 *            The new value of the force directed layout.
	 */
	public void setForceDirectedParameter(final float p_value) {
		this.m_fsim.getForces()[0].setParameter(0, p_value);
	}

	/**
	 * Returns the actionlist for the layout of our graph.
	 * 
	 * @return The actionlist containing the layout of the graph.
	 */
	private ActionList getLayoutActions() {
		// Make sure the nodes to not get to close together.
		this.m_fdl = new ForceDirectedLayout(PrefuseConstants.GRAPH);
		this.m_fsim = this.m_fdl.getForceSimulator();
		this.m_fsim.getForces()[0].setParameter(0, -10f);

		// Create an action list with an animated layout, the INFINITY parameter
		// tells the action list to run indefinitely.
		final ActionList layout = new ActionList(Activity.INFINITY);

		// Add the force directed layout.
		layout.add(this.m_fdl);

		layout.add(new HideDecoratorAction(this.m_vis));

		// Add the repaint action.
		layout.add(new RepaintAction());

		// Create te action which takes care of the coloring of the nodes which
		// are under the mouse.
		layout.add(new NodeColorAction(PrefuseConstants.GRAPH_NODES, this.m_vis));

		// Add the LabelLayout for the labels of the edges.
		layout.add(new LabelLayout(PrefuseConstants.EDGE_DECORATORS, this.m_vis));

		return layout;
	}

	/**
	 * This method creates the keyword searchpanel.
	 */
	private void createSearchPanel() {
		// Create a search panel for the tree.
		this.m_search = new JSearchPanel(this.m_vis, PrefuseConstants.GRAPH_NODES, Visualization.SEARCH_ITEMS,
				PrefuseConstants.TREE_NODE_LABEL, true, true);
		this.m_search.setShowResultCount(true);
		this.m_search.setBorder(BorderFactory.createEmptyBorder(5, 5, 4, 0));
		this.m_search.setFont(FontLib.getFont("Tahoma", Font.PLAIN, 11));
		this.m_search.setBackground(PrefuseConstants.BACKGROUND);
		this.m_search.setForeground(PrefuseConstants.FOREGROUND);
	}

	/**
	 * This method creates the title label.
	 */
	private void createTitleLabel() {
		// Create a label for the title of the nodes.
		this.m_URILabel = new JFastLabel("                 ");
		this.m_URILabel.setPreferredSize(new Dimension(350, 20));
		this.m_URILabel.setVerticalAlignment(SwingConstants.BOTTOM);
		this.m_URILabel.setBorder(BorderFactory.createEmptyBorder(3, 0, 0, 0));
		this.m_URILabel.setFont(FontLib.getFont("Tahoma", Font.PLAIN, 12));
		this.m_URILabel.setBackground(PrefuseConstants.BACKGROUND);
		this.m_URILabel.setForeground(PrefuseConstants.FOREGROUND);

		// The control listener for changing the title of a node at a mouseover event.
		addControlListener(new ControlAdapter() {
			@Override
			public void itemEntered(final VisualItem item, final MouseEvent e) {
				if (item.canGetString("URI")) {
					GraphDisplay.this.m_URILabel.setText(item.getString("URI"));
				}
			}
			@Override
			public void itemExited(final VisualItem item, final MouseEvent e) {
				GraphDisplay.this.m_URILabel.setText(null);
			}
		});
	}

	/**
	 * Returns the keyword searchpanel.
	 * 
	 * @return The keyword searchpanel.
	 */
	public JSearchPanel getSearchPanel() {
		return this.m_search;
	}

	/**
	 * Returns the title label.
	 * 
	 * @return The title label.
	 */
	public JFastLabel getTitleLabel() {
		return this.m_URILabel;
	}

	/**
	 * Return the GraphDistanceFilter.
	 * 
	 * @return The GraphDistanceFilter.
	 */
	public GraphDistanceFilter getDistanceFilter() {
		return this.m_filter;
	}
}